package com.szjlc.blog.common.interceptor;

import cn.hutool.core.bean.BeanUtil;
import com.szjlc.blog.common.entity.ExplainResultDTO;
import com.szjlc.blog.common.utils.PrettyFormatterSqlUtil;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.annotation.Resource;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

/**
 * mybatis-sql拦截器
 */
@Intercepts({@Signature(
        type = Executor.class,
        method = "update",
        args = {MappedStatement.class, Object.class}
), @Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
), @Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
)})
@Component
public class MybatisLogInterceptor implements Interceptor {

    private static final Logger log = LoggerFactory.getLogger(MybatisLogInterceptor.class);

    private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss:SSS");

    private Map<String, String> exceptionResultMap = new HashMap<>(16);

    private Properties properties;

    private static final String DEFAULT_SQL_LOG_SHOW_SWITCH = "false";

    // 开关，灵活配置，explain 也需要加个开关，默认值关闭，需要时才打开
    @Value("${sqlLogShowSwitch:true}")
    private Boolean sqlLogShowSwitch;

    @Value("${explainSqlLogShowSwitch:false}")
    private Boolean explainSqlLogShowSwitch;

    @Resource
    private JdbcTemplate jdbcTemplate;

    public Object intercept(Invocation invocation) throws Throwable {
        if (Boolean.FALSE.equals(this.sqlLogShowSwitch)) {
            return invocation.proceed();

        }
        else {
            try {
                Object[] args = invocation.getArgs();
                MappedStatement mappedStatement = (MappedStatement) args[0];
                Object parameter = null;
                if (args.length > 1) {
                    parameter = args[1];
                }

                String sqlId = mappedStatement.getId();
                BoundSql boundSql = null;
                if (args.length == 6) {
                    boundSql = (BoundSql) args[5];
                }

                if (boundSql == null) {
                    boundSql = mappedStatement.getBoundSql(parameter);
                }

                Configuration configuration = mappedStatement.getConfiguration();
                String date = LocalDateTime.now().format(FORMATTER);
                long start = System.currentTimeMillis();
                Object returnValue = invocation.proceed();
                long time = System.currentTimeMillis() - start;

                printSql2Log(sqlId, boundSql, configuration, date, time);

                /* String returnStr = "";
                if (returnValue != null) {
                    // returnStr = SfJsonUtil.toJsonStr(returnValue);
                }

                log.info("执行sql结果:{}", returnStr.length() >= 2000 ? returnStr.substring(0, 2000) : returnStr); */
                return returnValue;
            }
            catch (Exception e) {
                log.error("执行sql报错!errMsg:{}", e.getMessage(), e);
                return invocation.proceed();
            }
        }
    }

    private void printSql2Log(String sqlId, BoundSql boundSql, Configuration configuration, String date, long time) {
        String sql = getSql(configuration, boundSql, sqlId, time);
        String printSql = sql.split("(\\^ZHIKE\\^)")[0] + "\n"
                + "#执行SQL==>"
                + (new PrettyFormatterSqlUtil()).getPrettySql(sql.split("(\\^ZHIKE\\^)")[1])
                + ";\n";

        String explainResult;
        if (Boolean.TRUE.equals(explainSqlLogShowSwitch)) {
            if (exceptionResultMap.containsKey(boundSql.getSql())) {
                explainResult = exceptionResultMap.get(boundSql.getSql());
            }
            else {
                String explainSql = "EXPLAIN " + printSql.split("#执行SQL==>")[1];
                Map<String, Object> explainMap = jdbcTemplate.queryForMap(explainSql);
                ExplainResultDTO explainResultDTO = BeanUtil.mapToBean(explainMap, ExplainResultDTO.class, true);

                explainResult = explainResultDTO.toString().split("\\[")[1];
                exceptionResultMap.put(boundSql.getSql(), explainResult);
            }

            log.info("#[{}] [thread={}] 开始执行sql==>{}#<==执行总耗时【{}ms】\n# region explain执行计划:[{}\n# endregion\n",
                    date, Thread.currentThread().getName(), printSql, time, explainResult);
        }
        else {
            // vm开关，执行计划sql不打印
            log.info("#[{}] [thread={}] 开始执行sql==>{}#<==执行总耗时【{}ms】\n",
                    date, Thread.currentThread().getName(), printSql, time);
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        this.properties = properties;
    }

    public static String getSql(Configuration configuration, BoundSql boundSql, String sqlId, long time) {
        String sql = showSql(configuration, boundSql);
        StringBuilder builder = new StringBuilder(100);
        builder.append(sqlId);
        builder.append("^ZHIKE^");
        builder.append(sql);
        builder.append("^ZHIKE^");
        builder.append(time);
        builder.append("ms");
        return builder.toString();
    }

    private static String getParameterValue(Object obj) {
        String value;
        if (obj instanceof String) {
            value = "'" + obj + "'";
        }
        else if (obj instanceof Date) {
            value = "'" + SIMPLE_DATE_FORMAT.format(obj) + "'";
        }
        else if (obj != null) {
            value = obj.toString();
        }
        else {
            value = "";
        }

        return value;
    }

    public static String showSql(Configuration configuration, BoundSql boundSql) {
        try {
            Object parameterObject = boundSql.getParameterObject();
            List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
            String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
            if (!parameterMappings.isEmpty() && parameterObject != null) {
                TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
                if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                    sql = sql.replaceFirst("\\?", getParameterValue(parameterObject));
                }
                else {
                    MetaObject metaObject = configuration.newMetaObject(parameterObject);
                    Iterator var7 = parameterMappings.iterator();

                    while (var7.hasNext()) {
                        ParameterMapping parameterMapping = (ParameterMapping) var7.next();
                        String propertyName = parameterMapping.getProperty();
                        Object obj;
                        if (metaObject.hasGetter(propertyName)) {
                            obj = metaObject.getValue(propertyName);
                            sql = sql.replaceFirst("\\?", getParameterValue(obj));
                        }
                        else if (boundSql.hasAdditionalParameter(propertyName)) {
                            obj = boundSql.getAdditionalParameter(propertyName);
                            sql = sql.replaceFirst("\\?", getParameterValue(obj));
                        }
                    }
                }
            }

            return sql;
        }
        catch (Exception var11) {
            return "";
        }
    }
}
